package com.atguigu.gmall.gateway.component;


import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 网关对所有请求认证的过滤器
 */
@Data
@ConfigurationProperties(prefix = "authinfo")
@Component
public class AuthFilter implements GlobalFilter {

    @Autowired
    StringRedisTemplate redisTemplate;

    private AntPathMatcher matcher = new AntPathMatcher();
    private String needLoginPassPattern = "/api/**/auth/**";
    //@Value("${authinfo.urls}") 简单取值
    /**
     * @ConfigurationProperties(prefix = "authinfo") 绑定属性 比 @Value 强大
     * 支持复杂
     */
    private Set<String> urls;

    /**
     * Webflux的编程方式（响应式编程）
     * @param exchange  包含了request\response
     * @param chain     filter链 chain.doFilter(request,response,chain)
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取到请求响应
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //所有请求
        //1、内部接口,拒绝访问
        String path = request.getURI().getPath();
        if (matcher.match("/**/inner/**",path)){
            //拒绝访问 PERMISSION(209, "没有权限")
            /**
             * {
             *     code:209,
             *     message:"没有权限"
             * }
             */
            return responseWriteData(response, ResultCodeEnum.PERMISSION);
            //return response.setComplete();//响应结束
        }

        //2、判断用户是否登录？如果登陆了，再检查是否合法
        //获取到当前用户id
        String userId = getUserId(request);
        if ("-1".equals(userId)){
            //登录了但是不合法，以前带了错误数据token，每次请求，都会重定向到原来的地址
            return responseWriteData(response,ResultCodeEnum.LOGIN_AUTH);
        }


        /**
         * 以下是需要登录的，由网关打到登录页
         */
        //3、系统规范    /api/**/auth/**,这些接口需要登录
        if (matcher.match(needLoginPassPattern,path) && StringUtils.isEmpty(userId)){
            //需要登录，但是前面返回的是null,没有登陆，返回错误
            return responseWithLocation(request, response);
            //return response.setComplete();
        }

        //4、为了增强扩展性，需要有一个配置，直接配置哪些需要登录，网关就自己校验
        if (urls.contains(path) && StringUtils.isEmpty(userId)){
            return responseWithLocation(request, response);
            //return response.setComplete();
        }

        ServerHttpRequest.Builder requestBuilder = request.mutate();

        //如果cookie中有userTempId临时用户Id;以请求头的方式往下透传
        String userTempId = getUserTempId(request);
        if (!StringUtils.isEmpty(userTempId)){
            requestBuilder.header("userTempId", userTempId);
        }

        //透传userId:给request的请求头上设置一个userId:12,请求再放行给其他微服务
        //要么null,要么真正的用户id
        if (!StringUtils.isEmpty(userId)){
            //添加自定义userId请求头
            //HttpHeaders headers = request.getHeaders();
            //mutate()克隆
            requestBuilder.header("userId", userId);
        }
        ServerHttpRequest build = requestBuilder.build();
        //包装模式
        ServerWebExchange exchangeMutate = exchange.mutate().request(build).response(response).build();

        //过滤器链继续往下
        return chain.filter(exchangeMutate);
    }

    private String getUserTempId(ServerHttpRequest request) {
        String userTempId = request.getHeaders().getFirst("userTempId");
        if (StringUtils.isEmpty(userTempId)){//兼容客户端
            //正常请求头的token的空的，安卓，ios
            HttpCookie cookie = request.getCookies().getFirst("userTempId");
            if (cookie!=null){
                userTempId = cookie.getValue();
            }
        }
        return userTempId;
    }

    /**
     * 重定向到登录页
     * @param request
     * @param response
     * @return
     */
    private Mono<Void> responseWithLocation(ServerHttpRequest request,ServerHttpResponse response) {
        //让浏览器自己重定向到登录页
        response.setStatusCode(HttpStatus.FOUND);
        //浏览器一看到状态码是302，自己解析响应头的Location字段，浏览器自己跳过去

        //登录的token是错误的，需要清除以前的登录cookie
        /**
         * 1、服务器命令浏览器-保存cookie,响应头会带 Set-cookie：token=xxxx 默认失效是session
         * response.addCookie(xxx);
         * 2、删除cookie Set-cookie : token=xxx 默认失效：0
         * 3、命令浏览器保存cookie的时候，如果时间>0，浏览器保存，如果=0，浏览器就销毁
         */
        ResponseCookie tokenCookie = ResponseCookie
                .from("token", "xxxx")
                .maxAge(0L).domain("gmall.com")
                .build();
        //删除旧的令牌
        response.addCookie(tokenCookie);
        //删除旧的登录信息
        ResponseCookie userInfoToken = ResponseCookie
                .from("userInfo", "xxxx")
                .maxAge(0L).domain("gmall.com")
                .build();
        response.addCookie(userInfoToken);

        //浏览器会自己删除指定的token

        HttpHeaders headers = response.getHeaders();
        String url = "http://passport.gmall.com/login.html?originUrl="+request.getURI().toString();
        headers.set(HttpHeaders.LOCATION,url);
        return response.setComplete();
    }

    /**
     * 返回用户token 获取用户id
     * @param request
     * @return
     */
    private String getUserId(ServerHttpRequest request) {
        //如果用户登陆了，就自己返回用户id,同时进行合法性校验
        String token = request.getHeaders().getFirst("token");
        if (StringUtils.isEmpty(token)){//兼容客户端
            //正常请求头的token的空的，安卓，ios
            HttpCookie cookie = request.getCookies().getFirst("token");
            if (cookie!=null){
                token = cookie.getValue();
            }
        }

        //拿到了token
        if (StringUtils.isEmpty(token)){
            return null;
        }else{
            //验证合法性
            String key = "user:login:"+token; //就能去redis查到用户信息
            String json = redisTemplate.opsForValue().get(key);
            if (StringUtils.isEmpty(json)){
                return "-1";
            }
            Map<String, String> map = null;
            try {
                map = new ObjectMapper().readValue(json, new TypeReference<Map<String, String>>() {
                });
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            //拿到redis中的用户id
            String userId = map.get("userId");
            String ipAddr = map.get("ipAddr");
            //放冒牌、
            String ipAddress = IpUtil.getGatwayIpAddress(request);
            if (ipAddress.equals(ipAddr)){
                return userId;
            }else {
                return "-1";
            }
        }
    }

    /**
     * 给浏览器响应没有权限的json
     * @param response
     * @param permission
     * @return
     */
    private Mono<Void> responseWriteData(ServerHttpResponse response, ResultCodeEnum permission) {
        /**
         * {
         *     code:209,
         *     message:"没有权限"
         * }
         * 给响应体放json串
         */

        response.setStatusCode(HttpStatus.OK);
        HttpHeaders headers = response.getHeaders();
        //headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set(HttpHeaders.CONTENT_TYPE,"application/json;charset=utf-8");

        //放数据,响应式编程
        /**
         * 普通编程，数据库查1号数据：Person person = dao.findById(1);
         * 数据库数据做成一个 Publisher. 非阻塞 Mono    Flux
         * dao.findById(1) ==> Publisher; =======  Consumer(person)->{}
         * ajax 回调机制
         */
        DataBufferFactory dataBufferFactory = response.bufferFactory();
        Result<Object> build = Result.build(null, permission);

        String s = null;
        try {
            //将result转为json
            s = new ObjectMapper().writeValueAsString(build);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //将json写出去
        //返回流式数据 Mono,Flux

        return response.writeWith(Mono.just(dataBufferFactory.wrap(s.getBytes())));
    }
}
